<?php

class Database_Table_Row implements ArrayAccess, IteratorAggregate
{

	/**
	 * The data for each column in the row (column_name => value).
	 * The keys must match the physical names of columns in the
	 * table for which this row is defined.
	 *
	 * @var array
	 */
	protected $_data = array();

	/**
	 * This is set to a copy of $_data when the data is fetched from
	 * a database, specified as a new tuple in the constructor, or
	 * when dirty data is posted to the database with save().
	 *
	 * @var array
	 */
	protected $_cleanData = array();

	/**
	 * Tracks columns where data has been updated. Allows more specific insert and
	 * update operations.
	 *
	 * @var array
	 */
	protected $_modifiedFields = array();

	/**
	 * Database_Table parent class or instance.
	 *
	 * @var Database_Table
	 */
	protected $_table = null;

	/**
	 * Connected is true if we have a reference to a live
	 * Database_Table object.
	 * This is false after the Rowset has been deserialized.
	 *
	 * @var boolean
	 */
	protected $_connected = true;

	/**
	 * A row is marked read only if it contains columns that are not physically
	 * represented within
	 * the database schema (e.g. evaluated columns/Database_Expr columns). This can
	 * also be passed
	 * as a run-time config options as a means of protecting row data.
	 *
	 * @var boolean
	 */
	protected $_readOnly = false;

	/**
	 * Name of the class of the Database_Table object.
	 *
	 * @var string
	 */
	protected $_tableClass = null;

	/**
	 * Primary row key(s).
	 *
	 * @var array
	 */
	protected $_primary;

	/**
	 * @var string data type
	 */
	protected $_dataType = '';

	/**
	 * Constructor.
	 *
	 * Supported params for $config are:-
	 * - table       = class name or object of type Database_Table
	 * - data        = values of columns in this row.
	 *
	 * @param  array $config OPTIONAL Array of user-specified config options.
	 * @return void
	 * @throws Database_Exception
	 */
	public function __construct(array $config = array())
	{
		if (isset($config['table']) && $config['table'] instanceof Database_Table)
		{
			$this -> _table = $config['table'];
			$this -> _tableClass = get_class($this -> _table);
		}
		elseif ($this -> _tableClass !== null)
		{
			$this -> _table = $this -> _getTableFromString($this -> _tableClass);
		}

		if (isset($config['data']))
		{
			if (!is_array($config['data']))
			{

				throw new Database_Exception('Data must be an array');
			}
			$this -> _data = $config['data'];
		}
		if (isset($config['stored']) && $config['stored'] === true)
		{
			$this -> _cleanData = $this -> _data;
		}

		if (isset($config['readOnly']) && $config['readOnly'] === true)
		{
			$this -> setReadOnly(true);
		}

		// Retrieve primary keys from table schema
		if (($table = $this -> _getTable()))
		{
			$info = $table -> info();
			$this -> _primary = (array)$info['primary'];
		}

		$this -> init();
	}

	/**
	 * Transform a column name from the user-specified form
	 * to the physical form used in the database.
	 * You can override this method in a custom Row class
	 * to implement column name mappings, for example inflection.
	 *
	 * @param string $columnName Column name given.
	 * @return string The column name after transformation applied (none by default).
	 * @throws Database_Exception if the $columnName is not a string.
	 */
	protected function _transformColumn($columnName)
	{
		if (!is_string($columnName))
		{

			throw new Database_Exception('Specified column is not a string');
		}
		// Perform no transformation by default
		return $columnName;
	}

	/**
	 * Retrieve row field value
	 *
	 * @param  string $columnName The user-specified column name.
	 * @return string             The corresponding column value.
	 * @throws Database_Exception if the $columnName is not a column in the row.
	 */
	public function __get($columnName)
	{
		$columnName = $this -> _transformColumn($columnName);
		if (!array_key_exists($columnName, $this -> _data))
		{

			throw new Database_Exception("Specified column \"$columnName\" is not in the row");
		}
		return $this -> _data[$columnName];
	}

	/**
	 * Set row field value
	 *
	 * @param  string $columnName The column key.
	 * @param  mixed  $value      The value for the property.
	 * @return void
	 * @throws Database_Exception
	 */
	public function __set($columnName, $value)
	{
		$columnName = $this -> _transformColumn($columnName);
		if (!array_key_exists($columnName, $this -> _data))
		{

			throw new Database_Exception("Specified column \"$columnName\" is not in the row");
		}
		$this -> _data[$columnName] = $value;
		$this -> _modifiedFields[$columnName] = true;
	}

	/**
	 * Unset row field value
	 *
	 * @param  string $columnName The column key.
	 * @return Database_Table_Row
	 * @throws Database_Exception
	 */
	public function __unset($columnName)
	{
		$columnName = $this -> _transformColumn($columnName);
		if (!array_key_exists($columnName, $this -> _data))
		{

			throw new Database_Exception("Specified column \"$columnName\" is not in the row");
		}
		if ($this -> isConnected() && in_array($columnName, $this -> _table -> info('primary')))
		{

			throw new Database_Exception("Specified column \"$columnName\" is a primary key and should not be unset");
		}
		unset($this -> _data[$columnName]);
		return $this;
	}

	/**
	 * Test existence of row field
	 *
	 * @param  string  $columnName   The column key.
	 * @return boolean
	 */
	public function __isset($columnName)
	{
		$columnName = $this -> _transformColumn($columnName);
		return array_key_exists($columnName, $this -> _data);
	}

	/**
	 * Store table, primary key and data in serialized object
	 *
	 * @return array
	 */
	public function __sleep()
	{
		return array(
			'_tableClass',
			'_primary',
			'_data',
			'_cleanData',
			'_readOnly',
			'_modifiedFields'
		);
	}

	/**
	 * Setup to do on wakeup.
	 * A de-serialized Row should not be assumed to have access to a live
	 * database connection, so set _connected = false.
	 *
	 * @return void
	 */
	public function __wakeup()
	{
		$this -> _connected = false;
	}

	/**
	 * Proxy to __isset
	 * Required by the ArrayAccess implementation
	 *
	 * @param string $offset
	 * @return boolean
	 */
	public function offsetExists($offset)
	{
		return $this -> __isset($offset);
	}

	/**
	 * Proxy to __get
	 * Required by the ArrayAccess implementation
	 *
	 * @param string $offset
	 * @return string
	 */
	public function offsetGet($offset)
	{
		return $this -> __get($offset);
	}

	/**
	 * Proxy to __set
	 * Required by the ArrayAccess implementation
	 *
	 * @param string $offset
	 * @param mixed $value
	 */
	public function offsetSet($offset, $value)
	{
		$this -> __set($offset, $value);
	}

	/**
	 * Proxy to __unset
	 * Required by the ArrayAccess implementation
	 *
	 * @param string $offset
	 */
	public function offsetUnset($offset)
	{
		return $this -> __unset($offset);
	}

	/**
	 * Initialize object
	 *
	 * Called from {@link __construct()} as final step of object instantiation.
	 *
	 * @return void
	 */
	public function init()
	{
	}

	/**
	 * Returns the table object, or null if this is disconnected row
	 *
	 * @return Database_Table|null
	 */
	public function getTable()
	{
		return $this -> _table;
	}

	/**
	 * Set the table object, to re-establish a live connection
	 * to the database for a Row that has been de-serialized.
	 *
	 * @param Database_Table $table
	 * @return boolean
	 * @throws Database_Exception
	 */
	public function setTable(Database_Table $table = null)
	{
		if ($table == null)
		{
			$this -> _table = null;
			$this -> _connected = false;
			return false;
		}

		$tableClass = get_class($table);
		if (!$table instanceof $this->_tableClass)
		{

			throw new Database_Exception("The specified Table is of class $tableClass, expecting class to be instance of $this->_tableClass");
		}

		$this -> _table = $table;
		$this -> _tableClass = $tableClass;

		$info = $this -> _table -> info();

		if ($info['cols'] != array_keys($this -> _data))
		{

			throw new Database_Exception('The specified Table does not have the same columns as the Row');
		}

		if (!array_intersect((array)$this -> _primary, $info['primary']) == (array)$this -> _primary)
		{

			throw new Database_Exception("The specified Table '$tableClass' does not have the same primary key as the Row");
		}

		$this -> _connected = true;
		return true;
	}

	/**
	 * Query the class name of the Table object for which this
	 * Row was created.
	 *
	 * @return string
	 */
	public function getTableClass()
	{
		return $this -> _tableClass;
	}

	/**
	 * Test the connected status of the row.
	 *
	 * @return boolean
	 */
	public function isConnected()
	{
		return $this -> _connected;
	}

	/**
	 * Test the read-only status of the row.
	 *
	 * @return boolean
	 */
	public function isReadOnly()
	{
		return $this -> _readOnly;
	}

	/**
	 * Set the read-only status of the row.
	 *
	 * @param boolean $flag
	 * @return boolean
	 */
	public function setReadOnly($flag)
	{
		$this -> _readOnly = (bool)$flag;
	}

	/**
	 * Returns an instance of the parent table's Database_Table_Select object.
	 *
	 * @return Database_Table_Select
	 */
	public function select()
	{
		return $this -> getTable() -> select();
	}

	/**
	 * Saves the properties to the database.
	 *
	 * This performs an intelligent insert/update, and reloads the
	 * properties with fresh data from the table on success.
	 *
	 * @return mixed The primary key value(s), as an associative array if the
	 *     key is compound, or a scalar if the key is single-column.
	 */
	public function save()
	{
		/**
		 * If the _cleanData array is empty,
		 * this is an INSERT of a new row.
		 * Otherwise it is an UPDATE.
		 */
		if (empty($this -> _cleanData))
		{
			return $this -> _doInsert();
		}
		else
		{
			return $this -> _doUpdate();
		}
	}

	/**
	 * @return mixed The primary key value(s), as an associative array if the
	 *     key is compound, or a scalar if the key is single-column.
	 */
	protected function _doInsert()
	{
		/**
		 * A read-only row cannot be saved.
		 */
		if ($this -> _readOnly === true)
		{

			throw new Database_Exception('This row has been marked read-only');
		}

		/**
		 * Run pre-INSERT logic
		 */
		$this -> _insert();

		/**
		 * Execute the INSERT (this may throw an exception)
		 */
		$data = array_intersect_key($this -> _data, $this -> _modifiedFields);
		$primaryKey = $this -> _getTable() -> insert($data);

		/**
		 * Normalize the result to an array indexed by primary key column(s).
		 * The table insert() method may return a scalar.
		 */
		if (is_array($primaryKey))
		{
			$newPrimaryKey = $primaryKey;
		}
		else
		{
			//ZF-6167 Use tempPrimaryKey temporary to avoid that zend encoding fails.
			$tempPrimaryKey = (array)$this -> _primary;
			$newPrimaryKey = array(current($tempPrimaryKey) => $primaryKey);
		}

		/**
		 * Save the new primary key value in _data.  The primary key may have
		 * been generated by a sequence or auto-increment mechanism, and this
		 * merge should be done before the _postInsert() method is run, so the
		 * new values are available for logging, etc.
		 */
		$this -> _data = array_merge($this -> _data, $newPrimaryKey);

		/**
		 * Run post-INSERT logic
		 */
		$this -> _postInsert();

		/**
		 * Update the _cleanData to reflect that the data has been inserted.
		 */
		$this -> _refresh();

		return $primaryKey;
	}

	/**
	 * @return mixed The primary key value(s), as an associative array if the
	 *     key is compound, or a scalar if the key is single-column.
	 */
	protected function _doUpdate()
	{
		/**
		 * A read-only row cannot be saved.
		 */
		if ($this -> _readOnly === true)
		{

			throw new Database_Exception('This row has been marked read-only');
		}

		/**
		 * Get expressions for a WHERE clause
		 * based on the primary key value(s).
		 */
		$where = $this -> _getWhereQuery(false);

		/**
		 * Run pre-UPDATE logic
		 */
		$this -> _update();

		/**
		 * Compare the data to the modified fields array to discover
		 * which columns have been changed.
		 */
		$diffData = array_intersect_key($this -> _data, $this -> _modifiedFields);

		/**
		 * Were any of the changed columns part of the primary key?
		 */
		$pkDiffData = array_intersect_key($diffData, array_flip((array)$this -> _primary));

		/**
		 * Execute the UPDATE (this may throw an exception)
		 * Do this only if data values were changed.
		 * Use the $diffData variable, so the UPDATE statement
		 * includes SET terms only for data values that changed.
		 */
		if (count($diffData) > 0)
		{
			$this -> _getTable() -> update($diffData, $where);
		}

		/**
		 * Run post-UPDATE logic.  Do this before the _refresh()
		 * so the _postUpdate() function can tell the difference
		 * between changed data and clean (pre-changed) data.
		 */
		$this -> _postUpdate();

		/**
		 * Refresh the data just in case triggers in the RDBMS changed
		 * any columns.  Also this resets the _cleanData.
		 */
		$this -> _refresh();

		/**
		 * Return the primary key value(s) as an array
		 * if the key is compound or a scalar if the key
		 * is a scalar.
		 */
		$primaryKey = $this -> _getPrimaryKey(true);
		if (count($primaryKey) == 1)
		{
			return current($primaryKey);
		}

		return $primaryKey;
	}

	/**
	 * Deletes existing rows.
	 *
	 * @return int The number of rows deleted.
	 */
	public function delete()
	{
		/**
		 * A read-only row cannot be deleted.
		 */
		if ($this -> _readOnly === true)
		{

			throw new Database_Exception('This row has been marked read-only');
		}

		$where = $this -> _getWhereQuery();

		/**
		 * Execute pre-DELETE logic
		 */
		$this -> _delete();

		/**
		 * Execute the DELETE (this may throw an exception)
		 */
		$result = $this -> _getTable() -> delete($where);

		/**
		 * Execute post-DELETE logic
		 */
		$this -> _postDelete();

		/**
		 * Reset all fields to null to indicate that the row is not there
		 */
		$this -> _data = array_combine(array_keys($this -> _data), array_fill(0, count($this -> _data), null));

		return $result;
	}

	public function getIterator()
	{
		return new ArrayIterator((array)$this -> _data);
	}

	/**
	 * Returns the column/value data as an array.
	 *
	 * @return array
	 */
	public function toArray()
	{
		return (array)$this -> _data;
	}

	/**
	 * Sets all data in the row from an array.
	 *
	 * @param  array $data
	 * @return Database_Table_Row Provides a fluent interface
	 */
	public function fromArray(array $data)
	{
		$data = array_intersect_key($data, $this -> _data);

		foreach ($data as $columnName => $value)
		{
			$this -> __set($columnName, $value);
		}

		return $this;
	}

	/**
	 * Refreshes properties from the database.
	 *
	 * @return void
	 */
	public function refresh()
	{
		return $this -> _refresh();
	}

	/**
	 * Retrieves an instance of the parent table.
	 *
	 * @return Database_Table
	 */
	protected function _getTable()
	{
		if (!$this -> _connected)
		{
			throw new Database_Exception('Cannot save a Row unless it is connected');
		}
		return $this -> _table;
	}

	/**
	 * Retrieves an associative array of primary keys.
	 *
	 * @param bool $useDirty
	 * @return array
	 */
	protected function _getPrimaryKey($useDirty = true)
	{
		if (!is_array($this -> _primary))
		{
			throw new Database_Exception("The primary key must be set as an array");
		}

		$primary = array_flip($this -> _primary);
		if ($useDirty)
		{
			$array = array_intersect_key($this -> _data, $primary);
		}
		else
		{
			$array = array_intersect_key($this -> _cleanData, $primary);
		}
		if (count($primary) != count($array))
		{
			throw new Database_Exception("The specified Table '$this->_tableClass' does not have the same primary key as the Row");
		}
		return $array;
	}

	/**
	 * Constructs where statement for retrieving row(s).
	 *
	 * @param bool $useDirty
	 * @return array
	 */
	protected function _getWhereQuery($useDirty = true)
	{
		$where = array();
		$db = $this -> _getTable() -> getAdapter();
		$primaryKey = $this -> _getPrimaryKey($useDirty);
		$info = $this -> _getTable() -> info();
		$metadata = $info[Database_Table::METADATA];

		// retrieve recently updated row using primary keys
		$where = array();
		foreach ($primaryKey as $column => $value)
		{
			$tableName = $db -> quoteIdentifier($info[Database_Table::NAME], true);
			$type = $metadata[$column]['DATA_TYPE'];
			$columnName = $db -> quoteIdentifier($column, true);
			$where[] = $db -> quoteInto("{$tableName}.{$columnName} = ?", $value, $type);
		}
		return $where;
	}

	/**
	 * Refreshes properties from the database.
	 *
	 * @return void
	 */
	protected function _refresh()
	{
		$where = $this -> _getWhereQuery();
		$row = $this -> _getTable() -> fetchRow($where);

		if (null === $row)
		{

			throw new Database_Exception('Cannot refresh row as parent is missing');
		}

		$this -> _data = $row -> toArray();
		$this -> _cleanData = $this -> _data;
		$this -> _modifiedFields = array();
	}

	/**
	 * Allows pre-insert logic to be applied to row.
	 * Subclasses may override this method.
	 *
	 * @return void
	 */
	protected function _insert()
	{
	}

	/**
	 * Allows post-insert logic to be applied to row.
	 * Subclasses may override this method.
	 *
	 * @return void
	 */
	protected function _postInsert()
	{
	}

	/**
	 * Allows pre-update logic to be applied to row.
	 * Subclasses may override this method.
	 *
	 * @return void
	 */
	protected function _update()
	{
	}

	/**
	 * Allows post-update logic to be applied to row.
	 * Subclasses may override this method.
	 *
	 * @return void
	 */
	protected function _postUpdate()
	{
	}

	/**
	 * Allows pre-delete logic to be applied to row.
	 * Subclasses may override this method.
	 *
	 * @return void
	 */
	protected function _delete()
	{
	}

	/**
	 * Allows post-delete logic to be applied to row.
	 * Subclasses may override this method.
	 *
	 * @return void
	 */
	protected function _postDelete()
	{
	}

	/**
	 * _getTableFromString
	 *
	 * @param string $tableName
	 * @return Database_Table
	 */
	protected function _getTableFromString($tableName)
	{

		if ($this -> _table instanceof Database_Table)
		{
			$tableDefinition = $this -> _table -> getDefinition();

			if ($tableDefinition !== null && $tableDefinition -> hasTableConfig($tableName))
			{
				return new Database_Table($tableName, $tableDefinition);
			}
		}

		// assume the tableName is the class name
		if (!class_exists($tableName))
		{
			try
			{

			}
			catch (Exception $e)
			{

				throw new Database_Exception($e -> getMessage(), $e -> getCode(), $e);
			}
		}

		$options = array();

		if (($table = $this -> _getTable()))
		{
			$options['db'] = $table -> getAdapter();
		}

		if (isset($tableDefinition) && $tableDefinition !== null)
		{
			$options[Database_Table::DEFINITION] = $tableDefinition;
		}

		return new $tableName($options);
	}

	/*abstraction method*/

	function getDataType()
	{
		return $this -> _dataType;
	}

	/**
	 * set data type for model
	 * @param string $data_type
	 * @return Database_Table_Row
	 */
	function setDataType($data_type)
	{
		$this -> _dataType = $data_type;
		return $this;
	}

	/**
	 * @return int|string
	 */
	function getId()
	{
		if (isset($this -> id))
		{
			return $this -> id;
		}
	}

	/**
	 * global unique idenity cross all table
	 * @return string
	 */
	function getDataId()
	{
		return $this -> getDataType() . '@' . $this -> getId();
	}

	public function isActive()
	{
		return isset($this -> is_active) ? $this -> is_active : NULL;
	}

	/**
	 * @return string
	 */
	function getName()
	{
		return isset($this -> name) ? $this -> name : '';
	}

	function getTitle()
	{
		return isset($this -> title) ? $this -> title : '';
	}

	function getCode()
	{
		return isset($this -> code) ? $this -> code : '';
	}

	function getHref()
	{
		return '';
	}

	function toLink()
	{
		return strtr('<a href="{href}" title="{title}">{title}</a>', array(
			'{href}' => $this -> getHref(),
			'{title}' => $this -> getTitle()
		));
	}

	/**
	 * get src of photo
	 * @param string photo
	 */
	function getPhotoSrc($tag)
	{
		return 'http://namnv.younetco.com/v4/static/misc/user.png';
	}

	/**
	 * get param of photo
	 */
	function toPhotoLink()
	{
		return strtr('<a href="{href}" data-id="{data-id}"><img tooltip="{title}" src="{src}" alt="{title}"/></a>', array(
			'{title}' => $this -> getTitle(),
			'{src}' => $this -> getPhotoSrc($tag),
			'{href}' => $this -> getHref(),
			'{data-id}' => $this -> getDataId(),
		));
	}

	function toPhoto($tag = 'm')
	{
		return strtr('<img src="{src}" alt="{title}"/>', array(
			'{title}' => $this -> getTitle(),
			'{src}' => $this -> getPhotoSrc($tag)
		));
	}

	function toString()
	{
		return $this -> getTitle();
	}

	function __toString()
	{
		return $this -> toAnchor();
	}

	public function isLocked()
	{
		return $this -> is_locked;
	}

	public function getSortOrder()
	{
		return $this -> sort_order;
	}

}
